import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { addEvent, removeEvent, throttle } from '@/utils';
import './index.scss';
import { DEFAULT_WIDTH, DEFAULT_HEIGHT, BUFFER, CONNECT_BORDER, NONE_ACTIVE } from './constans';
import EHeader from './e-header';
import InputArea from './input-area';
import RowItem from './row-item';
import ColItem from './col-item';
import CellItem from './cell-item';
import SelectLine from './select-line';

export default function Excel() {
  const { clientWidth, clientHeight } = document.documentElement;
  const [visibleRow, setVisibleRow] = useState(Math.floor(clientWidth / DEFAULT_WIDTH));
  const [visibleCol, setVisibleCol] = useState(Math.floor(clientHeight / DEFAULT_HEIGHT));

  useEffect(() => {
    const _handleResize = throttle(handleResize, 50);
    addEvent(window, 'resize', _handleResize);
    function handleResize() {
      const { clientWidth, clientHeight } = document.documentElement;
      setVisibleRow(Math.floor(clientWidth / DEFAULT_WIDTH));
      setVisibleCol(Math.floor(clientHeight / DEFAULT_HEIGHT));
    }
    return () => {
      removeEvent(window, 'resize', _handleResize);
    };
  }, []);

  const [rowStart, setRowStart] = useState(0);
  const [colStart, setColStart] = useState(0);

  const [startX, setStartX] = useState(0);
  const [startY, setStartY] = useState(0);
  const rowRef = useRef();
  const scrollX = useMemo(() => rowRef.current?.scrollLeft || 0, [rowRef]);
  const colRef = useRef();
  const scrollY = useMemo(() => colRef.current?.scrollTop || 0, [colRef]);
  const cellRef = useRef();

  const [rowWidth, setRowWidth] = useState({});
  const [colHeight, setColHeight] = useState({});

  const [active, setActive] = useState(NONE_ACTIVE);
  const [selected, setSelected] = useState(NONE_ACTIVE);
  const [cellValueList, setCellValueList] = useState({});

  const [fontList, setFontList] = useState({
    row: {},
    col: {},
    cell: {},
  });

  const [borderList, setBorderList] = useState({
    row: {},
    col: {},
    cell: {},
  });

  const [mergeList, setMergeList] = useState({});
  let mergeInfo = useMemo(() => {
    let mergeIndexList = [];
    const keyStrList = Object.keys(mergeList);
    keyStrList.forEach(key => {
      mergeIndexList = mergeIndexList.concat(key.split(','));
    });
    return { mergeIndexList, keyStrList };
  }, [mergeList]);

  const getCellIndex = useCallback((row, col) => col * (visibleRow + BUFFER) + row, [visibleRow]);

  const getStartIndex = useMemo(
    () => getCellIndex(rowStart, colStart),
    [getCellIndex, rowStart, colStart]
  );

  const getGridCol = useMemo(() => {
    const res = [];
    for (let i = rowStart; i < rowStart + visibleRow + BUFFER; i++) {
      res.push(rowWidth[i] || DEFAULT_WIDTH);
    }
    return res.map(item => `${item}px`).join(' ');
  }, [rowWidth, rowStart, visibleRow]);

  const getGridRow = useMemo(() => {
    const res = [];
    for (let i = colStart; i < colStart + visibleCol + BUFFER; i++) {
      res.push(colHeight[i] || DEFAULT_HEIGHT);
    }
    return res.map(item => `${item}px`).join(' ');
  }, [colHeight, colStart, visibleCol]);

  const rows = useMemo(() => {
    let res = [];
    for (let i = rowStart; i < rowStart + visibleRow + BUFFER; i++) {
      res.push(
        <RowItem
          key={i}
          index={i}
          isActive={active.type === 'row' && i >= active.start && i <= active.end}
          width={rowWidth[i] || DEFAULT_WIDTH}
          addRowWidth={w => setRowWidth({ ...rowWidth, [i]: w })}
        ></RowItem>
      );
    }
    return res;
  }, [active, rowStart, rowWidth, visibleRow]);

  const cols = useMemo(() => {
    let res = [];
    for (let i = colStart; i < colStart + visibleCol + BUFFER; i++) {
      res.push(
        <ColItem
          key={i}
          index={i}
          isActive={active.type === 'col' && i >= active.start && i <= active.end}
          height={colHeight[i] || DEFAULT_HEIGHT}
          addColHeight={h => setColHeight({ ...colHeight, [i]: h })}
        ></ColItem>
      );
    }
    return res;
  }, [active, colHeight, colStart, visibleCol]);

  const getCol = useCallback(
    i => Math.floor((i - getStartIndex) / (visibleRow + BUFFER) + colStart),
    [colStart, getStartIndex, visibleRow]
  );

  const getRow = useCallback(
    i => ((i - getStartIndex) % (visibleRow + BUFFER)) + rowStart,
    [getStartIndex, visibleRow, rowStart]
  );

  const cells = useMemo(() => {
    const endIndex = getCellIndex(rowStart + visibleRow + BUFFER, visibleCol + colStart + BUFFER);
    let res = [];
    for (let i = getStartIndex; i < endIndex; i++) {
      const row = getRow(i);
      const col = getCol(i);
      const position = `${row}*${col}`;
      const computedFont = {
        ...(fontList.row[row] || {}),
        ...(fontList.col[col] || {}),
        ...(fontList.cell[position] || {}),
      };
      const computedBorder = {
        ...(borderList.row[row] || {}),
        ...(borderList.col[col] || {}),
        ...(borderList.cell[position] || {}),
      };

      let gridStyle = {};
      if (mergeInfo.mergeIndexList.includes(position)) {
        const key = mergeInfo.keyStrList.find(key => key.split(',')[0] === position);
        if (key) {
          const { start, end } = mergeList[key];
          gridStyle = {
            gridColumnStart: start[0] - rowStart + 1,
            gridColumnEnd: end[0] - rowStart + 2,
            gridRowStart: start[1] - colStart + 1,
            gridRowEnd: end[1] - colStart + 2,
          };
        } else continue;
      }

      res.push(
        <CellItem
          row={row}
          col={col}
          key={position}
          computedFont={computedFont}
          gridStyle={gridStyle}
          borderStyle={computedBorder}
          value={cellValueList[position] || ''}
          setInputValue={e => setCellValueList({ ...cellValueList, [position]: e })}
        ></CellItem>
      );
    }
    return res;
  }, [
    borderList,
    colStart,
    fontList,
    getCol,
    getRow,
    getStartIndex,
    mergeInfo,
    mergeList,
    rowStart,
    visibleCol,
    visibleRow,
    getCellIndex,
    cellValueList,
  ]);

  function getXIndex(distance) {
    let sum = 0;
    let index = 0;
    for (let i = 0; i < rowStart + visibleRow + BUFFER; i++) {
      sum += rowWidth[i] || DEFAULT_WIDTH;
      if (sum > distance) {
        index = i;
        sum -= rowWidth[i] || DEFAULT_WIDTH;
        break;
      }
    }
    return { sum, index };
  }

  function getYIndex(distance) {
    let sum = 0;
    let index = 0;
    for (let i = 0; i < colStart + visibleCol + BUFFER; i++) {
      sum += colHeight[i] || DEFAULT_HEIGHT;
      if (sum > distance) {
        index = i;
        sum -= colHeight[i] || DEFAULT_HEIGHT;
        break;
      }
    }
    return { sum, index };
  }

  function cellScroll(e) {
    const { scrollTop, scrollLeft } = e.target;
    rowRef.current.scrollLeft = scrollLeft;
    colRef.current.scrollTop = scrollTop;
    const getX = getXIndex(scrollLeft);
    const getY = getYIndex(scrollTop);
    let rowS = getX.index;
    let colS = getY.index;
    setRowStart(rowS);
    setColStart(colS);
    const _startX = getX.sum;
    setStartX(_startX);
    const _startY = getY.sum;
    setStartY(_startY);
  }

  function handleRowMouseDown(e) {
    if (e.target.className === 'row-resize') {
      return;
    }
    const start = Number(e.target.getAttribute('data-row'));
    let _selected = { type: 'row', start, end: start };
    setActive(_selected);
    setSelected(_selected);

    const _handleMouseMove = throttle(handleMouseMove, 50);
    addEvent(document, 'mousemove', _handleMouseMove);
    addEvent(document, 'mouseup', handleMouseUp);

    function handleMouseMove(e) {
      const windowWidth = document.documentElement.clientWidth || document.body.clientWidth;
      if (e.clientX >= windowWidth * 0.8) {
        cellRef.current.scrollLeft += 50;
        cellScroll({ target: cellRef.current });
      }
      const end = Number(e.target.getAttribute('data-row'));
      if (!end) return;
      let _start, _end;
      [_start, _end] = end > start ? [start, end] : [end, start];
      _selected = { type: 'row', start: _start, end: _end };
      setActive(_selected);
    }

    function handleMouseUp() {
      setSelected(_selected);
      removeEvent(document, 'mousemove', _handleMouseMove);
      removeEvent(document, 'mouseup', handleMouseUp);
    }
  }

  function handleColMouseDown(e) {
    if (e.target.className === 'col-resize') {
      return;
    }
    const start = Number(e.target.getAttribute('data-col'));
    let _selected = { type: 'col', start, end: start };
    setActive(_selected);
    setSelected(_selected);

    const _handleMouseMove = throttle(handleMouseMove, 30);
    addEvent(document, 'mousemove', _handleMouseMove);
    addEvent(document, 'mouseup', handleMouseUp);

    function handleMouseMove(e) {
      const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
      if (e.clientY >= windowHeight * 0.85) {
        cellRef.current.scrollTop += 40;
        cellScroll({ target: cellRef.current });
      }
      const end = Number(e.target.getAttribute('data-col'));
      if (!end) return;
      let _start, _end;
      [_start, _end] = end > start ? [start, end] : [end, start];
      _selected = { type: 'col', start: _start, end: _end };
      setActive(_selected);
    }

    function handleMouseUp() {
      setSelected(_selected);
      removeEvent(document, 'mousemove', _handleMouseMove);
      removeEvent(document, 'mouseup', handleMouseUp);
    }
  }

  function handleCellMouseDown(e) {
    const startRow = Number(e.target.getAttribute('data-row'));
    const startCol = Number(e.target.getAttribute('data-col'));

    const defaultStart = [startRow, startCol];
    let start, end;
    const startPosition = `${startRow}*${startCol}`;
    if (mergeInfo.mergeIndexList.includes(startPosition)) {
      const key = mergeInfo.keyStrList.find(key => key.split(',').includes(startPosition));
      ({ start, end } = mergeList[key] || [defaultStart, defaultStart]);
    } else {
      start = defaultStart;
      end = defaultStart;
    }
    let _selected = { type: 'cell', start, end };
    setActive(_selected);
    setSelected(_selected);

    const _handleMouseMove = throttle(handleMouseMove, 10);
    addEvent(document, 'mousemove', _handleMouseMove);
    addEvent(document, 'mouseup', handleMouseUp);

    function handleMouseMove(e) {
      const windowWidth = document.documentElement.clientWidth || document.body.clientWidth;
      const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
      if (e.clientX >= windowWidth * 0.8) {
        cellRef.current.scrollLeft += 20;
      }
      if (e.clientY >= windowHeight * 0.85) {
        cellRef.current.scrollTop += 20;
      }
      cellScroll({ target: cellRef.current });
      const endRow = Number(e.target.getAttribute('data-row'));
      const endCol = Number(e.target.getAttribute('data-col'));
      if (!endRow || !endCol) {
        return;
      }
      const end = [endRow, endCol];
      const endPosition = `${endRow}*${endCol}`;
      let _start, _end;
      [_start, _end] = [
        [Math.min(start[0], end[0]), Math.min(start[1], end[1])],
        [Math.max(start[0], end[0]), Math.max(start[1], end[1])],
      ];
      if (
        mergeInfo.mergeIndexList.includes(startPosition) ||
        mergeInfo.mergeIndexList.includes(endPosition)
      ) {
        const key = mergeInfo.keyStrList.find(
          key => key.split(',').includes(startPosition) || key.split(',').includes(endPosition)
        );
        const mergeItem = mergeList[key];
        _selected = {
          type: 'cell',
          start: [Math.min(mergeItem.start[0], _start[0]), Math.min(mergeItem.start[1], _start[1])],
          end: [Math.max(mergeItem.end[0], _end[0]), Math.max(mergeItem.end[1], _end[1])],
        };
      } else {
        _selected = { type: 'cell', start: _start, end: _end };
      }
      setActive(_selected);
    }

    function handleMouseUp() {
      setSelected(_selected);
      removeEvent(document, 'mousemove', _handleMouseMove);
      removeEvent(document, 'mouseup', handleMouseUp);
    }
  }

  const handleFont = useCallback(
    (key, value) => {
      const { type, start, end } = selected;
      if (!type) return;
      const _fontList = { ...fontList };
      if (type !== 'cell') {
        let _value = fontList[type]?.[start]?.[key] === value ? '' : value;
        for (let i = start; i <= end; i++) {
          if (_fontList[type][i]) {
            Object.assign(_fontList[type][i], { [key]: _value });
          } else {
            _fontList[type][i] = { [key]: _value };
          }
        }
      } else {
        const startIndex = getCellIndex(start[0], start[1]);
        const endIndex = getCellIndex(end[0], end[1]);
        for (let i = startIndex; i <= endIndex; i++) {
          const col = getCol(i);
          const row = getRow(i);
          const position = `${row}*${col}`;
          let _value = fontList[type]?.[position]?.[key] === value ? '' : value;
          if (row > end[0] || row < start[0]) {
            continue;
          }
          if (_fontList[type][position]) {
            Object.assign(_fontList[type][position], { [key]: _value });
          } else {
            _fontList[type][position] = { [key]: _value };
          }
        }
      }
      setFontList(_fontList);
    },
    [selected, fontList, getCol, getRow, getCellIndex]
  );

  const handleMerge = useCallback(() => {
    const { type, start, end } = selected;
    if (type !== 'cell') return;
    if (start.join() === end.join()) return;
    const _mergeList = { ...mergeList };
    const startIndex = getCellIndex(start[0], start[1]);
    const endIndex = getCellIndex(end[0], end[1]);
    const res = [];
    for (let i = startIndex; i <= endIndex; i++) {
      const row = getRow(i);
      const col = getCol(i);
      if (row > end[0] || row < start[0]) {
        continue;
      }
      const position = `${row}*${col}`;
      if (mergeInfo.mergeIndexList.includes(position)) {
        const key = mergeInfo.keyStrList.find(key => key.split(',').includes(position));
        if (_mergeList[key]) {
          if (_mergeList[key].startIndex === startIndex && _mergeList[key].endIndex === endIndex) {
            delete _mergeList[key];
            setMergeList(_mergeList);
            return;
          } else {
            delete _mergeList[key];
          }
        }
      }
      res.push(position);
    }
    const resStr = res.join();
    _mergeList[resStr] = { start, end, startIndex, endIndex };
    setMergeList(_mergeList);
  }, [selected, getCellIndex, getCol, getRow, mergeInfo, mergeList]);

  const handleRowBorder = useCallback(
    borderType => {
      const { start, end } = selected;
      if (['borderTop', 'borderBottom'].includes(borderType)) {
        return;
      }
      let _borderList = { ...borderList.row };

      function addBorder(i, connect, key, endWith) {
        if (endWith) return;
        if (_borderList[i]?.[key]) {
          _borderList[i][key] = '';
        }
        const connectKey = CONNECT_BORDER[key || borderType];
        const borderStr = borderType === 'none' ? '' : '1px solid #000';
        const obj = { [connectKey]: borderStr };
        if (_borderList[connect]) {
          Object.assign(_borderList[connect], obj);
        } else {
          _borderList[connect] = obj;
        }
      }

      switch (borderType) {
        case 'borderLeft':
          addBorder(null, start - 1);
          break;
        case 'borderRight':
          addBorder(null, end + 1);
          break;

        case 'all':
        case 'none':
          for (let i = start; i <= end; i++) {
            addBorder(i, i - 1, 'borderLeft');
            addBorder(i, i + 1, 'borderRight');
            addBorder(i, i, 'borderTop');
            addBorder(i, i, 'borderDown');
          }
          break;
        case 'out':
          addBorder(null, start - 1, 'borderLeft');
          addBorder(null, end + 1, 'borderRight');
          break;
        case 'inner':
          for (let i = start; i <= end; i++) {
            addBorder(i, i + 1, 'borderRight', i === end);
            addBorder(i, i - 1, 'borderLeft', i === start);
            addBorder(i, i, 'borderTop');
            addBorder(i, i, 'borderDown');
          }
          break;
        default:
          break;
      }
      setBorderList({ ...borderList, row: _borderList });
    },
    [borderList, selected]
  );

  const handleColBorder = useCallback(
    borderType => {
      const { start, end } = selected;
      if (['borderLeft', 'borderRight'].includes(borderType)) {
        return;
      }
      let _borderList = { ...borderList.col };

      function addBorder(i, connect, key, endWith) {
        if (endWith) return;
        if (_borderList[i]?.[key]) {
          _borderList[i][key] = '';
        }
        const connectKey = CONNECT_BORDER[key || borderType];
        const borderStr = borderType === 'none' ? '' : '1px solid #000';
        const obj = { [connectKey]: borderStr };
        if (_borderList[connect]) {
          Object.assign(_borderList[connect], obj);
        } else {
          _borderList[connect] = obj;
        }
      }

      switch (borderType) {
        case 'borderTop':
          addBorder(null, start - 1);
          break;
        case 'borderBottom':
          addBorder(null, end + 1);
          break;

        case 'all':
        case 'none':
          for (let i = start; i <= end; i++) {
            addBorder(i, i - 1, 'borderTop');
            addBorder(i, i + 1, 'borderBottom');
            addBorder(i, i, 'borderLeft');
            addBorder(i, i, 'borderRight');
          }
          break;
        case 'out':
          addBorder(null, start - 1, 'borderTop');
          addBorder(null, end + 1, 'borderBottom');
          break;
        case 'inner':
          for (let i = start; i <= end; i++) {
            addBorder(i, i - 1, 'borderTop', i === start);
            addBorder(i, i + 1, 'borderBottom', i === end);
            addBorder(i, i, 'borderLeft');
            addBorder(i, i, 'borderRight');
          }
          break;
        default:
          break;
      }
      setBorderList({ ...borderList, col: _borderList });
    },
    [borderList, selected]
  );

  const handleCellBorder = useCallback(
    borderType => {
      const { start, end } = selected;
      const startIndex = getCellIndex(start[0], start[1]);
      const endIndex = getCellIndex(end[0], end[1]);

      let _borderList = { ...borderList.cell };

      function clearConnected(position, key) {
        if (_borderList[position]?.[key]) {
          _borderList[position][key] = '';
        }
      }

      function addBorder(position, connectRow, connectCol, key, endWith) {
        if (endWith && !['all', 'none'].includes(borderType)) return;
        clearConnected(position, key);
        const connectPos = `${connectRow}*${connectCol}`;
        const connectKey = CONNECT_BORDER[key];
        const borderStr = borderType === 'none' ? '' : '1px solid #000';
        const obj = { [connectKey]: borderStr };
        if (_borderList[connectPos]) {
          Object.assign(_borderList[connectPos], obj);
        } else {
          _borderList[connectPos] = obj;
        }
      }

      for (let i = startIndex; i <= endIndex; i++) {
        const row = getRow(i);
        const col = getCol(i);
        const position = `${row}*${col}`;
        if (row > end[0] || row < start[0]) {
          continue;
        }

        switch (borderType) {
          case 'borderTop':
            addBorder(position, row, col - 1, borderType, col !== start[1]);
            break;
          case 'borderLeft':
            addBorder(position, row - 1, col, borderType, row !== start[0]);
            break;
          case 'borderRight':
            addBorder(position, row + 1, col, borderType, row !== end[0]);
            break;
          case 'borderBottom':
            addBorder(position, row, col + 1, borderType, col !== end[1]);
            break;
          case 'all':
          case 'none':
          case 'out':
            addBorder(position, row, col - 1, 'borderTop', col !== start[1]);
            addBorder(position, row - 1, col, 'borderLeft', row !== start[0]);
            addBorder(position, row + 1, col, 'borderRight', row !== end[0]);
            addBorder(position, row, col + 1, 'borderBottom', col !== end[1]);
            break;
          case 'inner':
            addBorder(position, row, col - 1, 'borderTop', col === start[1]);
            addBorder(position, row - 1, col, 'borderLeft', row === start[0]);
            addBorder(position, row + 1, col, 'borderRight', row === end[0]);
            addBorder(position, row, col + 1, 'borderBottom', col === end[1]);
            break;

          default:
            break;
        }
      }
      setBorderList({ ...borderList, cell: _borderList });
    },
    [selected, borderList, getCellIndex, getCol, getRow]
  );

  const handleBorder = useCallback(
    borderType => {
      const { type } = selected;
      switch (type) {
        case 'cell':
          handleCellBorder(borderType);
          break;
        case 'row':
          handleRowBorder(borderType);
          break;
        case 'col':
          handleColBorder(borderType);
          break;
        default:
          break;
      }
    },
    [handleRowBorder, handleColBorder, handleCellBorder, selected]
  );

  return (
    <div className="excel">
      <EHeader
        handleMerge={handleMerge}
        handleFont={handleFont}
        handleBorder={handleBorder}
        activeType={active.type}
      ></EHeader>
      <InputArea
        selected={selected?.type === 'cell' ? selected.start : []}
        cellValueList={cellValueList}
        setCellValueList={setCellValueList}
      ></InputArea>
      <div className="content">
        <div className="corner"></div>
        <div className="row-container" ref={rowRef}>
          <div
            className=""
            style={{
              width: (rowStart + visibleRow + 2 * BUFFER) * DEFAULT_WIDTH,
            }}
          ></div>
          <div
            className="row-wrapper"
            style={{
              transform: `translate(${startX}px,0)`,
            }}
            onMouseDown={e => handleRowMouseDown(e)}
          >
            {rows}
          </div>
        </div>
        <div className="col-container" ref={colRef}>
          <div
            className=""
            style={{
              height: (colStart + visibleCol + 2 * BUFFER) * DEFAULT_HEIGHT,
            }}
          ></div>
          <div
            className="col-wrapper"
            style={{
              transform: `translate(0,${startY}px)`,
            }}
            onMouseDown={e => handleColMouseDown(e)}
          >
            {cols}
          </div>
        </div>
        <div className="cell-container" ref={cellRef} onScroll={e => cellScroll(e)}>
          <div
            className=""
            style={{
              width: (rowStart + visibleRow + 2 * BUFFER) * DEFAULT_WIDTH,
              height: (colStart + visibleCol + 2 * BUFFER) * DEFAULT_HEIGHT,
            }}
          ></div>
          <div
            className="cell-wrapper"
            style={{
              gridTemplateColumns: getGridCol,
              gridTemplateRows: getGridRow,
              transform: `translate(${startX}px,${startY}px)`,
            }}
            onMouseDown={e => handleCellMouseDown(e)}
          >
            {cells}
          </div>

          <SelectLine
            active={active}
            rowWidth={rowWidth}
            colHeight={colHeight}
            scrollX={scrollX}
            scrollY={scrollY}
          ></SelectLine>
        </div>
      </div>
    </div>
  );
}
